﻿#region Using Statements
using System;
using System.Diagnostics;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Graphics.PackedVector;
#endregion

namespace GameBase.Particles
{
    /// <summary>
    /// Basic properties needed to implement any 3D particle system.
    /// </summary>
    public class ParticleSystem3D : IDisposable
    {
        #region Fields

        GraphicsDevice GraphicsDevice;
        Effect effect;

        // maximum number of particles displayed at one time
        int maxParticles;

        // stores the particles for the CPU... circular queue
        Particle3D[] particleArray;

        // stores the particles for the GPU
        DynamicVertexBuffer vertexBuffer;

        // turns sets of four vertices into particle quads
        IndexBuffer indexBuffer;

        // regions within the circular queue...
        // ... pointers to the first particle of that region
        int ptrRetired;
        int ptrActive;
        int ptrNew;
        int ptrFree;

        EffectParameter ep_World;
        EffectParameter ep_View;
        EffectParameter ep_Projection;
        EffectParameter ep_CurrentTime;
        EffectParameter ep_Duration;
        EffectParameter ep_Gravity;
        EffectParameter ep_EndVelocity;
        EffectParameter ep_MinStartSize;
        EffectParameter ep_MaxStartSize;
        EffectParameter ep_MinEndSize;
        EffectParameter ep_MaxEndSize;
        EffectParameter ep_MinColor;
        EffectParameter ep_MaxColor;
        EffectParameter ep_Texture;

        float currentTime;
        int currentFrame;

        static Random Random = new Random();

        #endregion

        #region Matrices

        /// <summary>
        /// Sets the world matrix.
        /// </summary>
        public Matrix World
        {
            set { ep_World.SetValue(value); }
        }

        /// <summary>
        /// Sets the view matrix.
        /// </summary>
        public Matrix View
        {
            set { ep_View.SetValue(value); }
        }

        /// <summary>
        /// Sets the projection matrix.
        /// </summary>
        public Matrix Projection
        {
            set { ep_Projection.SetValue(value); }
        }

        #endregion

        #region Particles

        // backing fields
        TimeSpan duration;

        /// <summary>
        /// Sets the texture used by the particles of the system.
        /// </summary>
        public Texture2D Texture
        {
            set { ep_Texture.SetValue(value); }
        }

        /// <summary>
        /// Sets the duration of the particles of the system.
        /// </summary>
        public TimeSpan Duration
        {
            private get { return duration; }
            set { duration = value; ep_Duration.SetValue((float)duration.TotalSeconds); }
        }

        #endregion

        #region Velocity

        /// <summary>
        /// Sets the maximum horizontal velocity given for any particle.
        /// </summary>
        public float MaxHorizontalVelocity
        {
            private get;
            set;
        }

        /// <summary>
        /// Sets the minimum horizontal velocity given for any particle.
        /// </summary>
        public float MinHorizontalVelocity
        {
            private get;
            set;
        }

        /// <summary>
        /// Sets the maximum vertical velocity given for any particle.
        /// </summary>
        public float MaxVerticalVelocity
        {
            private get;
            set;
        }

        /// <summary>
        /// Sets the minimum vertical velocity given for any particle.
        /// </summary>
        public float MinVerticalVelocity
        {
            private get;
            set;
        }

        /// <summary>
        /// Sets the factor which controls how the velocity of any particle will change during its life.
        /// <para>A value of 1 will make particles keep going at the same velocity as when they were created.</para>
        /// <para>A value of 0 will make particles completely stop before they disappear.</para>
        /// <para>Values greater than 1 will make particles speed up over time.</para>
        /// </summary>
        public float EndVelocity
        {
            set { ep_EndVelocity.SetValue(value); }
        }

        /// <summary>
        /// Sets the direction and strength of the gravity force applied to the particles of the system.
        /// </summary>
        public Vector3 Gravity
        {
            set { ep_Gravity.SetValue(value); }
        }

        #endregion

        #region Color, Size

        /// <summary>
        /// Sets the maximum color value given for any particle.
        /// </summary>
        public Color MaxColor
        {
            set { ep_MaxColor.SetValue(value.ToVector4()); }
        }

        /// <summary>
        /// Sets the minimum color value given for any particle.
        /// </summary>
        public Color MinColor
        {
            set { ep_MinColor.SetValue(value.ToVector4()); }
        }

        /// <summary>
        /// Sets the maximum start size given for any particle.
        /// </summary>
        public float MaxStartSize
        {
            set { ep_MaxStartSize.SetValue(value); }
        }

        /// <summary>
        /// Sets the minimum start size given for any particle.
        /// </summary>
        public float MinStartSize
        {
            set { ep_MinStartSize.SetValue(value); }
        }

        /// <summary>
        /// Sets the maximum end size given for any particle.
        /// </summary>
        public float MaxEndSize
        {
            set { ep_MaxEndSize.SetValue(value); }
        }

        /// <summary>
        /// Sets the minimum end size given for any particle.
        /// </summary>
        public float MinEndSize
        {
            set { ep_MinEndSize.SetValue(value); }
        }

        /// <summary>
        /// Sets the blend state used to draw the ParticleSystem3D.
        /// </summary>
        public BlendState BlendState
        {
            private get;
            set;
        }

        #endregion

        #region Initialization

        /// <summary>
        /// Initializes a new instance of this class.
        /// </summary>
        /// <param name="graphicsDevice">The GraphicsDevice associated to this ParticleSystem3D.</param>
        /// <param name="effect">The Effect associated to this ParticleSystem3D.</param>
        /// <param name="maxParticles">The maximum number of particles displayed at one time.</param>
        public ParticleSystem3D(GraphicsDevice graphicsDevice, Effect effect, int maxParticles)
        {
            this.GraphicsDevice = graphicsDevice;
            this.effect = effect.Clone();
            this.maxParticles = maxParticles;

            ParticleArray();
            ParticleVertexBuffer();
            ParticleEffect();

            World = Matrix.Identity;
            View = Matrix.Identity;
            Projection = Matrix.Identity;

            Duration = TimeSpan.FromSeconds(1);
            MaxHorizontalVelocity = 0.0f;
            MinHorizontalVelocity = 0.0f;
            MaxVerticalVelocity = 0.0f;
            MinVerticalVelocity = 0.0f;
            Gravity = Vector3.Zero;
            EndVelocity = 1;

            MaxColor = Color.White;
            MinColor = Color.White;
            MaxStartSize = 100;
            MinStartSize = 100;
            MaxEndSize = 100;
            MinStartSize = 100;
            BlendState = BlendState.NonPremultiplied;
        }

        /// <summary>
        /// Initializes particle data for the CPU.
        /// </summary>
        private void ParticleArray()
        {
            particleArray = new Particle3D[maxParticles * 4];

            for (int i = 0; i < maxParticles; i++)
            {
                particleArray[i * 4 + 0].Corner = new Short2(-1, -1);
                particleArray[i * 4 + 1].Corner = new Short2(1, -1);
                particleArray[i * 4 + 2].Corner = new Short2(1, 1);
                particleArray[i * 4 + 3].Corner = new Short2(-1, 1);
            }
        }

        /// <summary>
        /// Initializes particle data for the GPU.
        /// </summary>
        private void ParticleVertexBuffer()
        {
            vertexBuffer = new DynamicVertexBuffer(GraphicsDevice, typeof(Particle3D), maxParticles * 4, BufferUsage.None);
            vertexBuffer.ContentLost += (sender, args) => { vertexBuffer.SetData(particleArray, 0, maxParticles * 4, SetDataOptions.NoOverwrite); };

            ushort[] indices = new ushort[maxParticles * 6];

            for (int i = 0; i < maxParticles; i++)
            {
                // top triangle
                indices[i * 6 + 0] = (ushort)(i * 4 + 0);
                indices[i * 6 + 1] = (ushort)(i * 4 + 3);
                indices[i * 6 + 2] = (ushort)(i * 4 + 2);

                // bottom triangle
                indices[i * 6 + 3] = (ushort)(i * 4 + 0);
                indices[i * 6 + 4] = (ushort)(i * 4 + 2);
                indices[i * 6 + 5] = (ushort)(i * 4 + 1);
            }

            indexBuffer = new IndexBuffer(GraphicsDevice, typeof(ushort), indices.Length, BufferUsage.WriteOnly);
            indexBuffer.SetData(indices);
        }

        /// <summary>
        /// Initializes the particle effect.
        /// </summary>
        private void ParticleEffect()
        {
            EffectParameterCollection parameters = effect.Parameters;

            // variable parameters
            ep_World = parameters["World"];
            ep_View = parameters["View"];
            ep_Projection = parameters["Projection"];
            ep_CurrentTime = parameters["CurrentTime"];

            // constant parameters
            ep_Duration = parameters["Duration"];
            ep_Gravity = parameters["Gravity"];
            ep_EndVelocity = parameters["EndVelocity"];
            ep_MinStartSize = parameters["MinStartSize"];
            ep_MaxStartSize = parameters["MaxStartSize"];
            ep_MinEndSize = parameters["MinEndSize"];
            ep_MaxEndSize = parameters["MaxEndSize"];
            ep_MinColor = parameters["MinColor"];
            ep_MaxColor = parameters["MaxColor"];
            ep_Texture = parameters["Texture"];
        }

        #endregion

        #region Dispose

        // dispose flag
        bool isDisposed = false;

        /// <summary>
        /// Immediately releases all resources used by this object.
        /// </summary>
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        /// <summary>
        /// Allows an object to try to free resources and perform other cleanup operations before it is reclaimed by garbage collection.
        /// </summary>
        ~ParticleSystem3D()
        {
            Dispose(false);
        }

        /// <summary>
        /// Releases the unmanaged resources used by this object and optionally releases the managed resources.
        /// </summary>
        /// <param name="disposing">true to release both managed and unmanaged resources; false to release only unmanaged resources.</param>
        protected virtual void Dispose(bool disposing)
        {
            if (!isDisposed)
            {
                if (disposing)
                {
                    if (vertexBuffer != null) { vertexBuffer.Dispose(); vertexBuffer = null; }
                    if (indexBuffer != null) { indexBuffer.Dispose(); indexBuffer = null; }
                    if (BlendState != null) { BlendState.Dispose(); BlendState = null; }
                }

                isDisposed = true;
            }
        }

        #endregion

        #region Update

        /// <summary>
        /// Called when the ParticleSystem3D needs to be updated.
        /// </summary>
        /// <param name="gameTime">Time elapsed since the last call to this method.</param>
        public void Update(GameTime gameTime)
        {
            currentTime += (float)gameTime.ElapsedGameTime.TotalSeconds;

            RetireActiveParticles();
            FreeRetiredParticles();

            // active and new regions empty... reset timer
            if (ptrActive == ptrFree)
                currentTime = 0;

            // retired region empty... reset timer
            if (ptrRetired == ptrActive)
                currentFrame = 0;
        }
        
        /// <summary>
        /// Retires active particles once passed their duration.
        /// </summary>
        private void RetireActiveParticles()
        {
            float duration = (float)Duration.TotalSeconds;

            while (ptrActive != ptrNew)
            {
                float age = currentTime - particleArray[ptrActive * 4].TimeStamp;

                if (age < duration)
                    break;

                // remember the time at which this particle was retired
                particleArray[ptrActive * 4].TimeStamp = currentFrame;

                // retire particle
                ptrActive = (ptrActive + 1) % maxParticles;
            }
        }

        /// <summary>
        /// Frees retired particles once they are not required by the GPU. 
        /// </summary>
        private void FreeRetiredParticles()
        {
            while (ptrRetired != ptrActive)
            {
                // check how long the particle has been kept around
                int age = currentFrame - (int)particleArray[ptrRetired * 4].TimeStamp;

                // the GPU is never supposed to get more than one frame behind the CPU
                if (age < 2)
                    break;

                // free particle
                ptrRetired = (ptrRetired + 1) % maxParticles;
            }
        }

        #endregion

        #region Draw

        /// <summary>
        /// Called when the ParticleSystem3D needs to be drawn.
        /// </summary>
        /// <param name="gameTime">Time elapsed since the last call to this method.</param>
        public void Draw(GameTime gameTime)
        {
            if (ptrNew != ptrFree) { AddNewParticles(); }

            // If there are any active particles, draw them now!
            if (ptrActive != ptrFree)
            {
                GraphicsDevice.BlendState = BlendState;
                GraphicsDevice.DepthStencilState = DepthStencilState.DepthRead;
                GraphicsDevice.RasterizerState = RasterizerState.CullCounterClockwise;

                ep_CurrentTime.SetValue(currentTime);

                GraphicsDevice.SetVertexBuffer(vertexBuffer);
                GraphicsDevice.Indices = indexBuffer;

                foreach (EffectPass pass in effect.CurrentTechnique.Passes)
                {
                    pass.Apply();

                    if (ptrActive < ptrFree)
                    {
                        // active particles in a consecutive range
                        GraphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0,
                                                             ptrActive * 4, (ptrFree - ptrActive) * 4,
                                                             ptrActive * 6, (ptrFree - ptrActive) * 2);
                    }
                    else
                    {
                        // active particles not in a consecutive range
                        GraphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0,
                                                             ptrActive * 4, (maxParticles - ptrActive) * 4,
                                                             ptrActive * 6, (maxParticles - ptrActive) * 2);

                        if (ptrFree > 0)
                        {
                            GraphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0,
                                                                 0, ptrFree * 4,
                                                                 0, ptrFree * 2);
                        }
                    }
                }
            }

            currentFrame++;
        }

        /// <summary>
        /// Loads new particles onto the vertex buffer.
        /// </summary>
        private void AddNewParticles()
        {
            if (ptrNew < ptrFree)
            {
                // new particles in a consecutive range
                vertexBuffer.SetData(ptrNew * Particle3D.Size * 4, particleArray,
                                     ptrNew * 4, (ptrFree - ptrNew) * 4,
                                     Particle3D.Size, SetDataOptions.NoOverwrite);
            }
            else
            {
                // new particles not in a consecutive range
                vertexBuffer.SetData(ptrNew * Particle3D.Size * 4, particleArray,
                                     ptrNew * 4, (maxParticles - ptrNew) * 4,
                                     Particle3D.Size, SetDataOptions.NoOverwrite);

                if (ptrFree > 0)
                {
                    vertexBuffer.SetData(0, particleArray,
                                         0, ptrFree * 4,
                                         Particle3D.Size, SetDataOptions.NoOverwrite);
                }
            }

            ptrNew = ptrFree;
        }

        #endregion

        #region AddParticles

        /// <summary>
        /// Adds a new particle to the system.
        /// </summary>
        /// <param name="position">Starting position of the particle.</param>
        /// <param name="velocity">Starting velocity of the particle.</param>
        public void AddParticle(Vector3 position, Vector3 velocity)
        {
            // room for new particles?
            int nextPtrFree = (ptrFree + 1) % maxParticles;
            if (nextPtrFree == ptrRetired) return;

            float auxHrzVelocity = MathHelper.Lerp(MinHorizontalVelocity, MaxHorizontalVelocity, (float)Random.NextDouble());
            double auxHrzAngle = Random.NextDouble() * MathHelper.TwoPi;
            velocity.X += auxHrzVelocity * (float)Math.Cos(auxHrzAngle);
            velocity.Z += auxHrzVelocity * (float)Math.Sin(auxHrzAngle);
            velocity.Y += MathHelper.Lerp(MinVerticalVelocity, MaxVerticalVelocity, (float)Random.NextDouble());

            Color random = new Color((byte)Random.Next(255), (byte)Random.Next(255), (byte)Random.Next(255), (byte)Random.Next(255));

            for (int i = 0; i < 4; i++)
            {
                particleArray[ptrFree * 4 + i].Position = position;
                particleArray[ptrFree * 4 + i].Velocity = velocity;
                particleArray[ptrFree * 4 + i].Random = random;
                particleArray[ptrFree * 4 + i].TimeStamp = currentTime;
            }

            ptrFree = nextPtrFree;
        }

        #endregion
    }
}
